use std::{
    ffi::OsString,
    path::{Path, PathBuf},
};

use notify::Watcher;
use tokio::{
    fs,
    process::{Child, Command},
    select, sync, task,
};

use crate::{Configure, Error, Result};

/// scenario_runner进程控制.
///
/// 启动一个scenario_runner进程,此进程会连接到carla并加载指定场景.
pub struct Process {
    child: Child,
    w_rx: sync::oneshot::Receiver<()>,
    r_tx: sync::oneshot::Sender<()>,
}

impl Process {
    pub async fn new(
        config: &Configure,
        scenario_name: &str,
        is_openscenario: bool,
        is_pacakage: bool,
    ) -> Result<(Self, ProcessKiller)> {
        let mut cmd = Command::new(if cfg!(target_os = "windows") {
            "py"
        } else {
            "python3"
        });
        if cfg!(target_os = "windows") {
            cmd.arg("-3");
        }
        cmd.current_dir(config.scenario_runner_path.as_path())
            .env("CARLA_ROOT", config.carla_path.as_os_str())
            .env("PYTHONPATH", {
                let mut py_lib_path =
                    std::env::var_os("PYTHONPATH").map_or(OsString::from(""), |v| v);

                let mut egg_dir = config.carla_path.clone();
                egg_dir.push("PythonAPI");
                egg_dir.push("carla");
                egg_dir.push("dist");
                let egg_path = get_egg_path(&egg_dir).await?;

                py_lib_path.push(if cfg!(target_os = "windows") {
                    ";"
                } else {
                    ":"
                });
                py_lib_path.push(egg_path.as_os_str());

                py_lib_path
            })
            .arg("scenario_runner.py")
            .arg("--host")
            .arg(config.carla_addr.ip().to_string())
            .arg("--port")
            .arg(config.carla_addr.port().to_string())
            .arg("--timeout")
            .arg((config.carla_timeout_ms as f32 / 1e3).to_string())
            .arg("--reloadWorld");
        if is_openscenario {
            let scenario_path = if is_pacakage {
                config.tmp_scenario_package_dir.join("scenario.xosc")
            } else {
                config.tmp_openscenario_path.clone()
            };
            cmd.arg("--openscenario").arg(scenario_path.as_os_str());
        } else if is_pacakage {
            cmd.arg("--configFile")
                .arg(
                    config
                        .tmp_scenario_package_dir
                        .join("config.xml")
                        .as_os_str(),
                )
                .arg("--additionalScenario")
                .arg(
                    config
                        .tmp_scenario_package_dir
                        .join("scenario.py")
                        .as_os_str(),
                )
                .arg("--mapDir")
                .arg(config.tmp_scenario_package_dir.as_os_str())
                .arg("--scenario")
                .arg(scenario_name);
        } else {
            cmd.arg("--mapDir")
                .arg(
                    config
                        .scenario_runner_path
                        .join("srunner")
                        .join("map")
                        .as_os_str(),
                )
                .arg("--configFile")
                .arg(config.tmp_scenario_cfg_path.as_os_str())
                .arg("--scenario")
                .arg(scenario_name);
        }
        cmd.arg("--output");
        cmd.kill_on_drop(true);

        let notify_file_dir = config.tmp_scenario_package_dir.clone();
        fs::create_dir_all(notify_file_dir.as_path())
            .await
            .map_err(Error::from)?;
        let notify_file_path = notify_file_dir.join("notify");
        cmd.arg("--notifyFile").arg(notify_file_path.as_os_str());
        let (n_tx, n_rx) = std::sync::mpsc::channel();
        let mut watcher = notify::raw_watcher(n_tx).map_err(Error::from)?;
        watcher
            .watch(notify_file_dir.as_path(), notify::RecursiveMode::Recursive)
            .map_err(Error::from)?;

        let (w_tx, w_rx) = sync::oneshot::channel();
        let (r_tx, r_rx) = sync::oneshot::channel();
        let mut child = cmd.spawn().map_err(Error::from)?;

        // 等待场景加载完成
        select! {
            _ = task::spawn_blocking(move || { n_rx.recv().ok(); }) => {}

            _ = child.wait() => {
                return Err(Error::new("Start scenario failed".into()));
            }
        }

        Ok((Self { child, w_rx, r_tx }, ProcessKiller { w_tx, r_rx }))
    }

    /// 等待进程结束.
    pub async fn wait(mut self) -> Result<ScenarioResult> {
        let r = select! {
            r = self.child.wait() => {
                if let Some(code) = r.map_err(Error::from)?.code() {
                    if code >= 0 {
                        Ok(ScenarioResult {
                            success: code == 0
                        })
                    } else {
                        Err(Error::new("Process exit unsuccessfully".into()))
                    }
                } else {
                    Err(Error::new("Process has not ret code".into()))
                }
            }

            _ = self.w_rx => {
                self.child.kill().await.ok();
                Err(Error::new("Kill process".into()))
            }
        };

        self.r_tx.send(()).ok();

        r
    }
}

/// 场景运行结果.
pub struct ScenarioResult {
    pub success: bool,
}

/// 用于终止进程.
pub struct ProcessKiller {
    w_tx: sync::oneshot::Sender<()>,
    r_rx: sync::oneshot::Receiver<()>,
}

impl ProcessKiller {
    /// 终止进程.
    pub async fn kill(self) {
        self.w_tx.send(()).ok();
        self.r_rx.await.ok();
    }
}

async fn get_egg_path<P: AsRef<Path>>(dir: P) -> Result<PathBuf> {
    let mut dir = fs::read_dir(dir.as_ref())
        .await
        .map_err(|_| Error::new("Open dir error".into()))?;

    loop {
        let entry = dir
            .next_entry()
            .await
            .map_err(|_| Error::new("Read dir error".into()))?;

        if let Some(entry) = entry {
            let path = entry.path();
            if let Some(ext) = path.extension() {
                if ext == "egg" {
                    return Ok(path);
                }
            }
        } else {
            break;
        }
    }

    Err(Error::new("Can not find egg".into()))
}
